﻿using RockBus.Configuration.Interfaces;
using RockBus.DataContracts;
using RockBus.Messages.Interfaces;
using RockBus.ServiceModel;
using RockBus.Threading;
using RockBus.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading.Tasks;
using System.Transactions;

namespace RockBus.Messages
{
    public class MessageContext : IMessageContext
    {
        private static readonly ResponseFactory ResponseFactory = new ResponseFactory();

        private MessageContext(IInternalConfiguration configuration)
        {
            this.Configuration = configuration;
            this.TimeoutHelper = new TimeoutHelper<IMessageContext>(this.WaitOrTimerCallback, this, configuration.MessageTimeoutMs);
            this.PropertyBag = new Dictionary<string, object>();
        }

        private MessageContext(IInternalConfiguration configuration, ProcessingData processingData)
            : this(configuration)
        {
            this.ProcessingData = processingData;
        }

        public MessageContext(IInternalConfiguration configuration, IMessage message, Transaction transaction, RoutingHeader routingHeader, CorrelationHeader correlationHeader, TaskCompletionSource<Message> tcs, ExchangeTypeEnum publisherExchangeType, ReceivePortInfo receivePortInfo, bool throwOnNoSubscriptions = true)
            : this(configuration)
        {
            this.ThrowOnNoSubscriptions = throwOnNoSubscriptions;
            if (correlationHeader != null)
            {
                this.ProcessingData = new ProcessingData(correlationHeader.CorrelationId, correlationHeader.MessageId, publisherExchangeType, message.Version);
                this.ProcessingData.MessageDirection = MessageDirectionEnum.InboundResponse;
                this.ResponseMessage = message;
            }
            else if (routingHeader != null)
            {
                // We need to fill in the MessageVersion of the RoutingHeader from the Message when it comes in from Persistence queues
                routingHeader.ProcessingData.PublisherMessageVersion = message.Version;

                this.ProcessingData = routingHeader.ProcessingData;
                this.RoutingHeaders = new[] { routingHeader };

                if (routingHeader.ProcessingData.IsRequest)
                {
                    this.RequestMessage = message;
                }
                else
                {
                    this.ResponseMessage = message;
                }
            }
            else
            {
                var messageId = message.Properties.GetMessageId();
                if (!messageId.HasValue)
                {
                    messageId = Guid.NewGuid();
                }
                this.ProcessingData = new ProcessingData(Guid.NewGuid(), messageId.Value, publisherExchangeType, message.Version);
                this.RequestMessage = message;
            }

            this.Transaction = transaction;
            this.PublisherTcs = tcs;
            this.ReceivePortInfo = receivePortInfo;
        }

        public MessageContext(IInternalConfiguration configuration, IMessage message, RoutingHeader routingHeader)
            : this(configuration, routingHeader.ProcessingData)
        {
            if (routingHeader.ProcessingData.IsRequest)
            {
                this.RequestMessage = message;
            }
            else
            {
                this.ResponseMessage = message;
            }

            this.RoutingHeaders = new[] { routingHeader };
        }

        public MessageContext(IInternalConfiguration configuration, ProcessingData processingData, IMessage requestMessage, RoutingHeader routingHeader, TaskCompletionSource<Message> tcs, EvaluationResult evaluationResult, ReceivePortInfo receivePortInfo)
            : this(configuration, new ProcessingData(processingData))
        {
            this.RequestMessage = requestMessage;
            this.RoutingHeaders = new[] { routingHeader };
            this.PublisherTcs = tcs;
            this.EvaluationResult = evaluationResult;
            this.ReceivePortInfo = receivePortInfo;
            routingHeader.ProcessingData = this.ProcessingData;
        }

        public MessageContext(IInternalConfiguration configuration, ProcessingData processingData, IMessage requestMessage, Transaction transaction, IEnumerable<RoutingHeader> persistentRoutingHeaders, TaskCompletionSource<Message> tcs, EvaluationResult evaluationResult, ReceivePortInfo receivePortInfo)
            : this(configuration, new ProcessingData(processingData))
        {
            this.RequestMessage = requestMessage;
            this.Transaction = transaction;
            this.PublisherTcs = tcs;
            this.EvaluationResult = evaluationResult;
            this.ReceivePortInfo = receivePortInfo;

            if (persistentRoutingHeaders != null)
            {
                this.RoutingHeaders = persistentRoutingHeaders.ToArray();
                foreach (var rh in this.RoutingHeaders)
                {
                    rh.ProcessingData = this.ProcessingData;
                }
            }
        }

        public  IInternalConfiguration Configuration { get; set; }

        public ProcessingData ProcessingData { get; set; }

        public IMessage RequestMessage { get; set; }

        public IMessage ResponseMessage { get; set; }

        public IMessage Message
        {
            get
            {
                if (this.ProcessingData.IsRequest)
                {
                    return this.RequestMessage;
                }

                return this.ResponseMessage;
            }
        }

        public Transaction Transaction { get; private set; }

        public TaskCompletionSource<Message> PublisherTcs { get; private set; }

        public ReceivePortInfo ReceivePortInfo { get; set; }

        public EvaluationResult EvaluationResult { get; set; }

        public IEnumerable<RoutingHeader> RoutingHeaders { get; private set; }

        public Exception Exception { get; set; }

        public void SetProperty(string key, object value)
        {
            this.PropertyBag[key] = value;
        }

        public bool GetProperty(string key, out object value)
        {
            value = null;
            if (this.PropertyBag.ContainsKey(key))
            {
                value = this.PropertyBag[key];
                return true;
            }
            return false;
        }

        private Dictionary<string, object> PropertyBag { get; set; }

        private void WaitOrTimerCallback(IMessageContext state, bool isTimedOut)
        {
            if (isTimedOut)
            {
                this.Exception = new TimeoutException("MessageContext Timed out.");
                this.Abort();
            }
        }

        public void SetRoutingHeaders(IEnumerable<DeliveryInfo> deliveryInfos)
        {
            var rhs = new List<RoutingHeader>();
            foreach (var di in deliveryInfos)
            {
                var newProcessingData = new ProcessingData(
                    this.ProcessingData.CorrelationId,
                    Guid.NewGuid(),
                    this.ProcessingData.PublisherExchangeType,
                    this.ProcessingData.PublisherMessageVersion,
                    this.ProcessingData.MessageDirection);
                var rh = new RoutingHeader(this.ProcessingData, di);
                rhs.Add(rh);
            }
            this.RoutingHeaders = rhs;
        }

        public void SetRoutingHeader(RoutingHeader routingHeader)
        {
            if (routingHeader != null)
            {
                this.RoutingHeaders = new List<RoutingHeader>() { routingHeader };
            }
            else
            {
                this.RoutingHeaders = new List<RoutingHeader>();
            }
        }

        public bool IsPersistent()
        {
            if (this.RoutingHeaders.IsNullOrEmpty())
            {
                return false;
            }

            var first = this.RoutingHeaders.First();
            return first.IsPersistent;
        }

        public bool IsCorrelatedSubscriberEndpoint()
        {
            var first = this.RoutingHeaders.FirstOrDefault();
            return (first != null) && first.DestinationEndpoint.IsCorrelated;
        }

        public bool ThrowOnNoSubscriptions { get; private set; }

        public TransactionScope CreateTransactionScope(TransactionScopeOption option)
        {
            switch (option)
            {
                case TransactionScopeOption.Required:
                    return this.CreateTransactionScope();

                case TransactionScopeOption.RequiresNew:
                    if (this.Transaction != null)
                    {
                        throw new ArgumentException("Trying to create a new transaction while one already exists.");
                    }
                    return this.CreateTransactionScope();

                case TransactionScopeOption.Suppress:
                    return new TransactionScope(option);//, TransactionScopeAsyncFlowOptions.Enabled);
                default:
                    throw new ArgumentException("Invalid TransactionScopeOption.");
            }
        }

        /// <summary>
        /// Satisfies the PublisherTcs (if not null).
        ///  Commits the Transaction if it is owned by the MessageContext instance.
        /// </summary>
        /// <returns>true if there were no Exceptions. false if there were exceptions and the MessageContext instance needs to be Aborted.</returns>
        public bool Complete()
        {
            try
            {
                this.TimeoutHelper.Complete();

                // Technically, there's a race condition on this.Exception
                //  (because the Timeout can come just before the above call to this.TimeoutHelper.Complete()
                //  However, once we get past this point, we consider the MessageContext to be completed in time and ignore the Exception.
                if (this.Exception != null)
                {
                    return false;
                }

                bool setPublisherTcsResultSucceeded = true;
                if (this.PublisherTcs != null)
                {
                    switch (this.ProcessingData.PublisherExchangeType)
                    {
                        case ExchangeTypeEnum.OneWay:
                        case ExchangeTypeEnum.OneWayRequestResponse:
                            // If we have a PublisherTcs here, it exists only in order to complete the send operation.
                            setPublisherTcsResultSucceeded = this.PublisherTcs.TrySetResult(null);
                            break;

                        case ExchangeTypeEnum.TwoWayAck:
                        case ExchangeTypeEnum.TwoWayAckRequestResponse:
                            if ((this.ReceivePortInfo != null) && (!string.IsNullOrEmpty(this.ReceivePortInfo.ResponseId)))
                            {
                                var ri = this.ReceivePortInfo.ResponseInfo;
                                var preconfiguredResponse = ri.CreateResponseBuffer(this.ProcessingData.PublisherMessageVersion);
                                // The AdvertisedResponse can also be an automatically generated WCF TwoWayVoid response
                                setPublisherTcsResultSucceeded = this.PublisherTcs.TrySetResult(preconfiguredResponse.CreateMessage());
                            }
                            else if ((this.ReceivePortInfo != null) && (!this.ReceivePortInfo.GenerateDefaultResponse))
                            {
                                // If the publisher has indicated through an ReceivePort to want TwoWayAck,
                                //  but did specify GenerateResponse==false
                                //  We return the RockBusAck.
                                var ack = RockBusAck.CreateMessage(this.ProcessingData.PublisherMessageVersion);
                                setPublisherTcsResultSucceeded = this.PublisherTcs.TrySetResult(ack);
                            }
                            else
                            {
                                var defaultResponse = ResponseFactory.CreateResponseBuffer(this.RequestMessage.MessageBuffer);
                                setPublisherTcsResultSucceeded = this.PublisherTcs.TrySetResult(defaultResponse.CreateMessage());
                            }
                            break;

                        case ExchangeTypeEnum.TwoWayRequestResponse:
                            if (this.ResponseMessage != null)
                            {
                                // Give back the subscriber response
                                var response = this.ResponseMessage.MarshalMessage(this.ProcessingData.PublisherMessageVersion);
                                setPublisherTcsResultSucceeded = this.PublisherTcs.TrySetResult(response);
                            }
                            else if ((this.ReceivePortInfo == null) || (this.ReceivePortInfo.GenerateDefaultResponse))
                            {
                                var defaultResponse = ResponseFactory.CreateResponseBuffer(this.RequestMessage.MessageBuffer);
                                setPublisherTcsResultSucceeded = this.PublisherTcs.TrySetResult(defaultResponse.CreateMessage());
                            }
                            else
                            {
                                var ack = RockBusAck.CreateMessage(this.ProcessingData.PublisherMessageVersion);
                                setPublisherTcsResultSucceeded = this.PublisherTcs.TrySetResult(ack);
                            }
                            break;

                        default:
                            break;
                    }
                }

                if (!setPublisherTcsResultSucceeded)
                {
                    return false;
                }

                this.PublisherTcs = null;

                if (this._ownsTransaction && (this.Transaction != null))
                {
                    ((CommittableTransaction)this.Transaction).Commit();
                    this.Transaction.Dispose();
                    this.Transaction = null;
                }
                return true;
            }
            catch (Exception exc)
            {
                this.Exception = exc;
                return false;
            }
        }

        public void Abort()
        {
            if (this._ownsTransaction && (this.Transaction != null))
            {
                ((CommittableTransaction)this.Transaction).Rollback();
                this.Transaction.Dispose();
                this.Transaction = null;
                this._ownsTransaction = false;
            }

            if (this.PublisherTcs != null)
            {
                // If RoutingHeaders is empty, this is an incoming message to which we're satisfying the result
                if (this.RoutingHeaders.IsNullOrEmpty())
                {
                    switch (this.ProcessingData.PublisherExchangeType)
                    {
                        case ExchangeTypeEnum.OneWay:
                        case ExchangeTypeEnum.OneWayRequestResponse:
                            // Set the exception, so that any non-owned transactions are rolled back
                            if (this.Exception != null)
                            {
                                this.PublisherTcs.TrySetException(this.Exception);
                            }
                            else
                            {
                                this.PublisherTcs.TrySetException(new Exception("AbortedException"));
                            }
                            break;

                        case ExchangeTypeEnum.TwoWayAck:
                        case ExchangeTypeEnum.TwoWayAckRequestResponse:
                        case ExchangeTypeEnum.TwoWayRequestResponse:
                            string faultAction = null;
                            System.ServiceModel.Channels.MessageFault fault = null;

                            // If we have a response message, and it's a fault, return that
                            if ((this.ResponseMessage != null) &&
                                (this.ResponseMessage.IsFault))
                            {
                                faultAction = this.ResponseMessage.Message.Headers.Action;
                                fault = MessageFault.CreateFault(this.ResponseMessage.Message, 10000);
                            }
                            else
                            {
                                faultAction = ResponseFactory.GetDefaultFaultAction(this.RequestMessage.Message);
                                fault = MessageFault.CreateFault(
                                    new FaultCode("InternalServerError"),
                                    "An internal error occurred while processing the message. Please verify the logfiles or the ErrorMessage table for details.");
                            }

                            var faultMessage = System.ServiceModel.Channels.Message.CreateMessage(
                                this.RequestMessage.Version,
                                fault,
                                faultAction);
                            this.PublisherTcs.TrySetResult(faultMessage);
                            break;

                        default:
                            break;
                    }
                }
                else
                {
                    // Set the exception, so that any non-owned transactions are rolled back
                    if (this.Exception != null)
                    {
                        this.PublisherTcs.TrySetException(this.Exception);
                    }
                    else
                    {
                        this.PublisherTcs.TrySetException(new Exception("AbortedException"));
                    }
                }

                this.PublisherTcs = null;
            }

            this.ProcessingData.MessageDirection = MessageDirectionEnum.End;
        }

        private bool _ownsTransaction;

        private TransactionScope CreateTransactionScope()
        {
            if (this.Transaction == null)
            {
                var to = new TransactionOptions();
                to.IsolationLevel = this.Configuration.IsolationLevel;
                to.Timeout = TimeSpan.FromMilliseconds(this.Configuration.TransactionTimeoutMs);

                this.Transaction = new CommittableTransaction(to);
                this._ownsTransaction = true;
            }

            var ts = new TransactionScope(this.Transaction);
            return ts;
        }

        public override string ToString()
        {
            var rh = (this.RoutingHeaders == null) ? null : this.RoutingHeaders.FirstOrDefault();
            if (this.Exception == null)
            {
                return string.Format("{0}, {1}, {2}.",
                    this.ProcessingData,
                    this.RequestMessage,
                    rh == null ? string.Empty : rh.ToString());
            }
            else
            {
                return string.Format("{0}, {1}, {2}. EXCEPTION: {3}",
                    this.ProcessingData,
                    this.RequestMessage,
                    rh == null ? string.Empty : rh.ToString(),
                    (this.Exception == null) ? string.Empty : this.Exception.ToString());
            }
        }

        private TimeoutHelper<IMessageContext> TimeoutHelper { get; set; }
    }
}